// MIT License (MIT)
// 
// Copyright (c) 2014 K Ercan Turkarslan
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// 

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using CodeGen;
using CollectionsLibrary;

namespace MiosGenerator
{
    public class DeviceFile : MiosFile
    {
        public String DeviceType { get; private set; }
        public ItemsCollection<ServiceNode> ServiceNodes { get; private set; }

        #region ImplementedItems Collection and Parser
        public ItemsCollection<SupportedItem> ImplementedItems { get; private set; }

        internal void ParseImplementationDetails(XNode deviceNode)
        {
            if (ImplementedItems != null)
                return;

            ImplementedItems = new ItemsCollection<SupportedItem>();

            foreach (var childNode in deviceNode.GetChildren("states"))
            {
                var serviceId = childNode.Attributes("service");
                var variable = childNode.Attributes("variable");

                var serviceNode = ServiceNodes.GetByName(serviceId);

                if (serviceNode == null)
                {
                    serviceNode = MiosFiles.FindServiceNode(serviceId);
                    if (serviceNode == null)
                        continue;

                    ServiceNodes.Add(new ServiceNode(serviceNode), false);
                }

                var stateVariable = serviceNode.ServiceDescription.StateVariables.GetByName(variable);

                if (stateVariable == null)
                {
                    stateVariable = new StateVariable(variable, "string");
                    serviceNode.ServiceDescription.StateVariables.Add(stateVariable, false);
                }

                ImplementedItems.Add(new SupportedItem(serviceId, stateVariable), false);
            }

            if (MiosFiles.Engine == null)
                return;

            UInt32 deviceNum;

            if (!UInt32.TryParse(deviceNode.Attributes("id"), out deviceNum))
                return;

            var actionsXml = XDocument.Parse(MiosFiles.Engine.Luup_Actions(deviceNum, "xml"));

            if (actionsXml.Root == null)
                return;

            var serviceLists = actionsXml.Root.Element("serviceList");
            if (serviceLists == null)
                return;

            foreach (var serviceList in serviceLists.Nodes())
            {
                var serviceId = serviceList.Attributes("serviceId");
                var serviceNode = ServiceNodes.GetByName(serviceId);

                foreach (var action in serviceList.GetChildren("actionList")
                                            .Select(actionNode => actionNode.Attributes("name"))
                                            .Select(actionName => serviceNode.ServiceDescription.Actions.GetByName(actionName))
                                            .Where(action => action != null))
                {
                    ImplementedItems.Add(new SupportedItem(serviceId, action), false);
                }
            }
        }
        #endregion

        #region Generate Method
        private void GenerateConstructor(CodeBlock cb, Boolean isBasic, Int32 numberOfServices)
        {
            // Generate Type Constructor for Device only implementation
            // e.g.: D_BinaryLight1.xml
            cb.Constructor_Begin(
                "public", 
                ClassName, 
                isBasic 
                    ? "MiosEngine engine, UInt32 deviceId" 
                    : "MiosEngine engine, UInt32 deviceId, String name, String localUdn, String deviceType, MiosRoom room", 
                isBasic ? "base(engine, deviceId)" : "base(engine, deviceId, name, localUdn, deviceType, room)");

            cb.Insert(numberOfServices == 0 ? "" : "InitializeInheritedServices(engine, deviceId);");

            cb.Constructor_End();
        }

        private void GenerateSetVariableMethod(CodeBlock cb)
        {
            cb.Method_Begin("internal override", "SetVariable", "String serviceId, String variable, String value", "Boolean");

                cb.Switch_Begin("serviceId");

                    foreach (var serviceNode in ServiceNodes.SortedItems)
                    {
                        cb.Switch_Case(serviceNode.ServiceId);
                        cb.Switch_Case_Return(String.Format("S{0}.SetVariable(variable, value)", serviceNode.ServiceClassName));
                    }

                    cb.Switch_Case("urn:micasaverde-com:serviceId:ZWaveDevice1");
                    cb.Switch_Case_Return("SZWaveDevice1.SetVariable(variable, value)");
                    cb.NewLine();

                    cb.Switch_Default();
                    cb.Switch_Case_Return("false");

                cb.Switch_End();

            cb.Method_End("SetVariable");
        }

        private void GenerateSetShortCodeMethod(CodeBlock cb)
        {
            var shortCodeCount = ServiceNodes.SortedItems
                    .SelectMany(serviceNode => serviceNode.ServiceDescription.StateVariables.SortedItems).Count();

            if (shortCodeCount == 0)
                return;

            cb.Method_Begin("internal override", "SetShortCode", "String shortCode, String value", "Boolean");

            cb.Switch_Begin("shortCode");

                var shortCodes = new HashSet<String>();

                foreach (var serviceNode in ServiceNodes.SortedItems)
                {
                    foreach (var stateVariable in serviceNode.ServiceDescription.StateVariables.SortedItems)
                    {
                        if (String.IsNullOrEmpty(stateVariable.ShortCode))
                            continue;

                        var shortCode = stateVariable.ShortCode;
                        if (shortCodes.Contains(shortCode))
                        {
                            cb.NewLine();
                            cb.Comment(
                                "WARNING: Duplicate short code found! Commenting out second instance.",
                                String.Format(
                                        "case \"{0}\": return S{1}.SetVariable(variable, value);",
                                        shortCode,
                                        serviceNode.ServiceClassName));
                        }
                        else
                        {
                            cb.Switch_Case(shortCode);
                            cb.Switch_Case_Return(
                                    String.Format("SetVariable(\"{0}\", \"{1}\", value)",
                                    serviceNode.ServiceId,
                                    stateVariable.Name));

                            shortCodes.Add(shortCode);
                        }
                    }
                }

                cb.Switch_Default();
                cb.InsertIndented("return false;");

            cb.Switch_End();

            cb.Method_End("SetShortCode");
        }

        internal void Generate(String generateFolder)
        {
            var cb = CodeBlock.NewCodeFile("K Ercan Turkarslan", "2014");

            cb.Using("System");
            cb.CommentSingle("ReSharper disable once RedundantUsingDirective");
            cb.Using("Mios.Generated.ServiceFiles");
            cb.NewLine();
            cb.Namespace_Begin("Mios.Generated.DeviceFiles");

            cb.Comment(String.Format("Generated from {0}", Filename));

            cb.ChildClass(ClassName, "MiosDevice");

                if (ImplementedItems != null)
                {
                    cb.Comment(
                        "This device only implements following Actions and State Variables from",
                        "referenced Service Descriptions.",
                        "",
                        "Following information was gathered from USER_DATA and LU_ACTIONS calls",
                        "for existing devices from Vera Engine specified during code generation.");
                    foreach (var property in ImplementedItems.SortedItems)
                    {
                        cb.CommentSingle(property.Type, property.Name);
                    }
                    cb.NewLine();
                }

                // Generate Inherited Service instances
                // e.g. : BinaryLight1.cs 
                //#region Inherited ServiceNodes
                //
                //public EnergyMetering1 EnergyMetering1 { get; private set; }        
                //public HaDevice1 HaDevice1 { get; private set; }        
                //public SwitchPower1 SwitchPower1 { get; private set; }
                //
                //#endregion

                var numberOfServices = 0;

                if (ServiceNodes.Count() > 0)
                {
                    cb.Region_Begin("Inherited ServiceNodes");
                    cb.NewLine();

                    foreach (var serviceNode in ServiceNodes.SortedItems)
                    {
                        if (!serviceNode.FromDescription)
                        {
                            cb.Comment(
                                "WARNING: This service definition was not found in the device file.",
                                "However an action or state variable from this Service was returned by",
                                "USER_DATA xml.");
                        }

                        cb.Insert(String.Format("public {0}Service S{1} {{ get; private set; }}",
                            serviceNode.ClassName,
                            serviceNode.ServiceClassName));

                        cb.NewLine();
                    }

                    cb.Method_Begin("private", "InitializeInheritedServices", "MiosEngine engine, UInt32 deviceId");
                        foreach (var serviceNode in ServiceNodes.SortedItems)
                        {
                            cb.Insert(
                                String.Format("S{0} = new {1}Service(engine, deviceId, \"{2}\");",
                                    serviceNode.ServiceClassName,
                                    serviceNode.ClassName,
                                    serviceNode.ServiceId));

                            numberOfServices++;
                        }
                    cb.Method_End("InitializeInheritedServices");

                    cb.Region_End("Inherited ServiceNodes");

                    GenerateSetVariableMethod(cb);
                    GenerateSetShortCodeMethod(cb);
                }

                cb.Region_Begin("Constructor(s)");

                GenerateConstructor(cb, true, numberOfServices);
                cb.NewLine();

                cb.Insert("#if USE_SDATA || USE_USERDATA");
                GenerateConstructor(cb, false, numberOfServices);
                cb.Insert("#endif");

                cb.Region_End("Constructor(s)");

            cb.Class_End();
            cb.Namespace_End();

            cb.WriteToFile(Path.Combine(generateFolder, "DeviceFiles", String.Format("{0}.cs", ClassName)));
        }

        #endregion

        #region ParseDeviceFile Method
        private void ParseDeviceFile(XDocument deviceFile)
        {
            if (deviceFile == null)
                throw new ArgumentNullException("deviceFile");

            DeviceType = deviceFile.Root.GetNode("device").GetValue("deviceType");

            ServiceNodes = new ItemsCollection<ServiceNode>();
            foreach (var childNode in deviceFile.Root.GetNode("device").GetChildren("serviceList"))
            {
                ServiceNodes.Add(new ServiceNode(childNode), false);
            }
        }
        #endregion

        #region Constructor(s)
        public DeviceFile(String filename)
            : base(filename)
        {
            if (MiosFiles.TypeOfMiosFile(filename) != MiosFiles.Types.DeviceFile)
                throw new InvalidOperationException(
                    String.Format("[{0}] is not a Service File! Expecting S_*.xml", FullFilename));

            //ClassName = MiosFiles.RetrieveClassName(filename);            

            try
            {
                ParseDeviceFile(XDocument.Load(FullFilename));
            }
            catch (System.Xml.XmlException)
            {
                throw new System.Xml.XmlException("Could not parse Xml file!");
            }            
        }
        #endregion
    }
}